/*
 * File     : EnumDictionary.java
 * Created  : 28 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal;

import java.lang.reflect.Field;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;

import com.googlecode.dni.type.cenum.CEnumUnknownValue;
import com.googlecode.dni.type.cenum.CEnumValue;


/**
 * <p>
 *  Encapsulates a two-way map of Java enum constant to C int enum value.
 * </p>
 * <p>
 *  Two implementations are provided: one for the sparse and/or large range, and
 *  one for a contiguous, small range.
 * </p>
 *
 * @author Matthew Wilson
 */
public abstract class EnumDictionary
{

    private static final Map< Class< ? >, EnumDictionary > DICTIONARIES =
        new HashMap< Class< ? >, EnumDictionary >();

    private final Map< ? extends Enum< ? >, Integer > enumToValueMap;

    private final Enum< ? > unknown;


    /**
     * @param enumToValueMap
     *            the map of enum constant to value
     * @param unknown
     *            the unknown value; possibly <code>null</code>
     */
    EnumDictionary( final Map< ? extends Enum< ? >, Integer > enumToValueMap, final Enum< ? > unknown )
    {
        this.enumToValueMap = enumToValueMap;
        this.unknown = unknown;
    }

    /**
     * Creates or fetches the enum dictionary for the given type.
     *
     * @param <E>
     *            the enum type
     * @param enumType
     *            the enum type
     * @return a dictionary
     */
    public static < E extends Enum< E > > EnumDictionary get( final Class< E > enumType )
    {
        synchronized ( DICTIONARIES )
        {
            EnumDictionary enumDictionary = DICTIONARIES.get( enumType );
            if ( enumDictionary != null )
            {
                return enumDictionary;
            }

            enumDictionary = create( enumType );
            DICTIONARIES.put( enumType, enumDictionary );
            return enumDictionary;
        }
    }

    /**
     * Looks up an enum constant for a C int value.
     *
     * @param value
     *            the integer value
     *
     * @return the enum constant; possibly <code>null</code>
     */
    public abstract Enum< ? > enumForValue( int value );

    /**
     * Looks up a C int value for an enum constant.
     *
     * @param constant
     *            the enum constant
     *
     * @return the integer value
     */
    public final int valueForEnum( final Enum< ? > constant )
    {
        Integer value = this.enumToValueMap.get( constant );
        return value == null ? 0 : value.intValue();
    }

    /**
     * @return the unknown value
     */
    final Enum< ? > getUnknown()
    {
        return this.unknown;
    }

    private static < E extends Enum< E > > EnumDictionary create( final Class< E > enumType )
    {
        E[] constants = enumType.getEnumConstants();
        Map< Integer, Enum< ? > > valueToEnumMap = new HashMap< Integer, Enum< ? > >( constants.length * 2 );
        Map< E, Integer > enumToValueMap = new EnumMap< E, Integer >( enumType );

        Enum< ? > unknown = null;

        int value = 0;
        int maxValue = Integer.MIN_VALUE;
        int minValue = Integer.MAX_VALUE;
        for ( final E constant : constants )
        {
            Field field;
            try
            {
                field = enumType.getField( constant.name() );
            }
            catch ( Exception exception )
            {
                // TODO IllegalEnumException or something
                throw new IllegalArgumentException( "Enum field ought to be present and accessible: "
                                                    + enumType.getName() + "." + constant );
            }

            CEnumValue cEnumValue = field.getAnnotation( CEnumValue.class );
            CEnumUnknownValue cEnumUnknownValue = field.getAnnotation( CEnumUnknownValue.class );

            if ( cEnumValue != null && cEnumUnknownValue != null )
            {
                // TODO IllegalEnumException or something
                throw new IllegalArgumentException( "Cannot have @CEnumValue and @CEnumUnknownValue: "
                                                    + enumType.getName() + "." + constant );
            }

            if ( cEnumValue != null )
            {
                value = cEnumValue.value();
            }

            if ( cEnumUnknownValue != null )
            {
                if ( unknown == null )
                {
                    unknown = constant;
                }
            }
            else
            {
                if ( !valueToEnumMap.containsKey( value ) )
                {
                    valueToEnumMap.put( value, constant );
                }
                enumToValueMap.put( constant, value );

                minValue = Math.min( value, minValue );
                maxValue = Math.max( value, maxValue );

                value++;
            }
        }

        int range = maxValue - minValue + 1;
        if ( range <= 256 )
        {
            return new ContiguousEnumDictionary( valueToEnumMap,
                                                 enumToValueMap,
                                                 unknown,
                                                 minValue,
                                                 maxValue );
        }

        return new SparseEnumDictionary( valueToEnumMap, enumToValueMap, unknown );
    }

}
